#include "Updater.h"


Updater::Updater()
{
	GetCurrentDirectory(MAX_PATH, dir);
	changeSet = 0;
}


Updater::~Updater(void)
{
	if(changeSet != 0)
		free(changeSet);
}


int Updater::calculateHashes(){
	std::list<struct serverData>::iterator it;
	char *hash;
	int result;
	result = listFiles();
	if(result != 0)
		return result;
	for(it = files.begin(); it != files.end(); it++){
		hash = generateHash((char*)(it->dir.empty()?it->file:(it->dir+"\\"+it->file)).c_str());
		if(hash == 0)
			return 2;
		it->hash = hash;
		free(hash);
	}
	return 0;
}


int Updater::listFiles(char *subdir, char *dir, char *root){
	WIN32_FIND_DATA fileData;
	HANDLE file;
	DWORD result;
	char path[MAX_PATH], local[MAX_PATH]={0};
	struct serverData add;
	struct directory ld;
	auto setPath = [&](){for(int i=0; i < MAX_PATH; i++){if(dir[i]=='*'){path[i]='\0';break;} path[i]=dir[i];}
		sprintf_s(&path[strlen(path)],MAX_PATH-strlen(path), "%s\\*", subdir); 
		strlen(root) == 0?sprintf_s(local, MAX_PATH, "%s", subdir):sprintf_s(local, MAX_PATH, "%s\\%s", root, subdir);};
	if(strlen(this->dir) == 0)
		return 1;
	if(subdir == 0){
		ld.dir = "";
		directories.push_back(ld);
		sprintf_s(path, MAX_PATH, "%s\\*", this->dir);
	}
	else{
		setPath();
		ld.dir = local;
		directories.push_back(ld);
	}
	file = FindFirstFile(path, &fileData);
	if(file == INVALID_HANDLE_VALUE)
		return 2;
	while('.' == fileData.cFileName[0])
		FindNextFile(file, &fileData);
	do{
		if(fileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
			listFiles(fileData.cFileName, path, local);
		else{
			if(strcmp(fileData.cFileName, UPDATER_EXE) == 0)
				continue;
			add.dir = local;
			//add.dir.erase(add.dir.end()-1);
			add.file = fileData.cFileName;
			files.push_back(add);
		}
	}while(FindNextFile(file, &fileData));
	result = GetLastError();
	return result != ERROR_NO_MORE_FILES ?  result :  0;
}


char* Updater::generateHash(char *file){
	std::ifstream in;
	HCRYPTPROV hProv = 0;
    HCRYPTHASH hHash = 0;
	BYTE *rgbHash = new BYTE[SHALEN];
	DWORD rgbHashLen = SHALEN;
	char lookup[] = "0123456789abcdef";
	char *result, buffer;
	int size;
	if(!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
		return 0;
	if(!CryptCreateHash(hProv, CALG_SHA1, 0, 0, &hHash)){
		CryptReleaseContext(hProv,0);
		return 0;
	}
	in.open(file, std::ios::in | std::ios::binary);
	while(true){
		in.read(&buffer, 1);
		if(in.eof()) break;
		if(!CryptHashData(hHash, (BYTE*)&buffer, 1, 0)){
			CryptDestroyHash(hHash);
			CryptReleaseContext(hProv, 0);
			return 0;
		}
	}
	result = 0;
retry:
	if(CryptGetHashParam(hHash, HP_HASHVAL, rgbHash, &rgbHashLen, 0)){
		result = (char*)malloc(rgbHashLen*2+1);
		for(DWORD i = 0, pos = 0; i < rgbHashLen; i++, pos += 2){
			sprintf_s(&result[pos], 3, "%c%c", lookup[rgbHash[i] >> 4], lookup[rgbHash[i] & 0xf]);
		}
	}
	else if(GetLastError() == ERROR_MORE_DATA){
		rgbHash = new BYTE[rgbHashLen];
		goto retry;
	}
	CryptDestroyHash(hHash);
	CryptReleaseContext(hProv, 0);
	return result;
}


int Updater::update(){
	std::list<struct serverData>::iterator it;
	std::list<struct directory>::iterator di;
	std::string sdir;
	char dir[MAX_PATH];

	for(di = directories.begin(); di != directories.end(); di++)
		if(di->op == operation::update){
			memset(dir, 0, MAX_PATH);
			sprintf_s(dir, MAX_PATH, "%s\\%s", this->dir, di->dir.c_str());
			CreateDirectory(dir, NULL);
		}

	for(it = files.begin(); it != files.end(); it++){
		switch(it->op){
		case operation::del:
			remove((it->dir+it->file).c_str());
			break;
		case operation::mov: break;
		case operation::update:
			sdir = "/";
			sdir += root+it->dir;
			for(std::string::iterator i = sdir.begin(); i != sdir.end(); i++)
				if(*i == '\\')
					*i = '/';
			c.requestFile((char*)sdir.c_str(), (char*)it->file.c_str());
			c.loadFile((char*)(it->dir.empty()?it->file:it->dir+"\\"+it->file).c_str());
			break;
		}
	}
	for(di = directories.begin(); di != directories.end(); di++)
		if(di->op == operation::del){
			memset(dir, 0, MAX_PATH);
			sprintf_s(dir, MAX_PATH, "%s\\%s", this->dir, di->dir.c_str());
			RemoveDirectory(dir);
		}
	return 0;
}


int Updater::loadVersion(){
	char *result;
	if(c.sendRequest(POST_VERSION))
		return 1;
	result = c.receive();
	currentVersion = atoi(result);
	free(result);
	return 0;
}


int Updater::loadChangeSet(){
	char aHeader[256], version[256] = {0};
	strcpy_s(version, 256, POST_VERSION);
	_itoa_s(localVersion, &version[strlen(version)], 256-strlen(version), 10);
	if(c.sendRequest(version) != 0)
		return 1;
	if(changeSet != 0)
		free(changeSet);
	changeSet = c.receive();
	return 0;
}


int Updater::run(){
	char *in;
	std::string post;
	std::list<std::string>::iterator it;
	u_char *version = (u_char*)&localVersion;
	printf_s("loading local version...\n");
	localVersion = system(GW2MAPSEXE_VERSION);
	printf_s("local version: %hhx.%hhx.%hhx.%hhx\n", version[3], version[2], version[1], version[0]);
	printf_s("collecting file data\n");
	if(calculateHashes() != 0){
		printf_s("ERROR:unable to collect file data\n");
		return 1;
	}
	printf_s("\tOK\ncontacting http://gw2location.chillerlan.net\n");
	if(c.connect() != 0){
		printf_s("ERROR:could not establish a connection to the internet\n");
		return 2;
	}
	printf_s("\tOK\nchecking for updates...\n");
	post = POST_FILECHECK;
	c.sendRequest((char*)post.c_str());
	in = c.receive();
	c.close();
	parseServerData(in);
	compareFiles();
	if(files.empty()){
		printf_s("no updates available\n");
		return 0;
	}
	printf_s("loading new files, this may take some minutes\n");
	update();
	return 0;
}


void Updater::parseServerData(char *data){
	struct serverData sd;
	char *buffer1, *buffer2;
	int si;
	int state = 0, pos = 0;
	if(data == 0)
		return;
	buffer1 = strstr(data, "Gw2Maps.exe");
	buffer2 = buffer1;
	while(*buffer1 != ':')
		buffer1--;
	buffer1++;
	while(pos < 128 && buffer1 != buffer2){
		root[pos] = *buffer1;
		buffer1++;
		pos++;
	}
	root[pos] = '\0';
	pos = 0;
	while(data[pos] != '\0'){
		sd.hash = ""; sd.dir = ""; sd.file = "";
		while(data[pos] != '\0' && data[pos] != ':'){
			sd.hash += data[pos];
			pos++;
		}
		pos++;
		pos += strlen(root);
		while(data[pos] != '\0' && data[pos] != '\r'){
			sd.dir += data[pos] == '/' ? '\\' : data[pos];
			pos++;
		}
		pos += 2;
		si = sd.dir.find_last_of('\\');
		si++;
		sd.file = sd.dir.substr(si);
		sd.dir.erase(sd.dir.begin()+si, sd.dir.end());
		if(si != 0)
			sd.dir.pop_back();
		if(sd.file.compare(UPDATER_EXE) != 0)
			s_data.push_back(sd);
	}
}


void Updater::compareFiles(){
	std::list<struct serverData> data;
	std::list<struct serverData>::iterator lt, ct;
	std::list<struct directory>::iterator dir;
	struct directory d;
	bool del;
	for(lt = files.begin(); lt != files.end(); lt++){
		del = true;
		for(ct = s_data.begin(); ct != s_data.end(); ct++){
			if(ct->file.compare(lt->file) == 0){
				if(ct->dir.compare(lt->dir) != 0){
					lt->op = operation::del;
					data.push_back(*lt);
					ct->op = operation::update;
					data.push_back(*ct);
				}
				else if(ct->hash.compare(lt->hash) != 0 ){
					ct->op = operation::update;
					data.push_back(*ct);
				}
				del = false;
				break;
			}
		}
		if(del){
			lt->op = operation::del;
			data.push_back(*lt);
		}
	}
	for(ct = s_data.begin(); ct != s_data.end(); ct++){
		del = false;
		for(dir = directories.begin(); dir != directories.end(); dir++){
			if(dir->dir.compare(ct->dir) == 0){
				del = true;
				dir->op = operation::nop;
				break;
			}
		}
		if(!del){
			d.dir = ct->dir;
			d.op = operation::update;
		}
		del = true;
		for(lt = files.begin(); lt != files.end(); lt++){
			if(ct->file.compare(lt->file) == 0){
				del = false;
				break;
			}
		}
		if(del){
			ct->op = operation::update;
			data.push_back(*ct);
		}
	}
	for(dir = directories.begin(); dir != directories.end(); dir++){
		del = true;
		for(ct = s_data.begin(); ct != s_data.end(); ct++){
			if(ct->dir.compare(dir->dir) == 0){
				del = false;
				break;
			}
		}
		if(del)
			dir->op = operation::del;
	}
	files.clear();
	files = data;
}